package com.googlecode.gaal.vis.impl;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import com.googlecode.gaal.vis.api.Drawing;
import com.googlecode.gaal.vis.api.EdgeStyle;
import com.googlecode.gaal.vis.api.NodeStyle;

public abstract class AbstractTikzDrawing implements Drawing {

    protected static final String FIGURE_HEADER = "\\begin{figure}[%s]\n";
    protected static final String FIGURE_FOOTER = "\\caption{%s}\n\\label{fig:%s}\n\\end{figure}\n";

    protected static final String TIKZ_HEADER = "\\begin{center}\n\\begin{tikzpicture}\n";
    protected static final String TIKZ_FOOTER = "\\end{tikzpicture}\n\\end{center}\n";

    private static final String HEADER = "[scale=%.2f, nodes={scale=%.2f, minimum size=%d},\n";

    private static final String FOOTER = "};\n";

    private static final String CELL_FORMAT = "\\node(%d) at (%d,%.1f) [%s] {%s};\n";
    private static final String LABEL_FORMAT = "\\textbf{%s}";
    private static final String NODE_FORMAT = "\\node(%d) at (%d,%.1f) [%s] {%s};\n";
    private static final String INDEXED_LABEL_FORMAT = LABEL_FORMAT + "$_{%s}$";
    protected static final double NODE_Y_SCALE = 1.5;

    private static final String EDGE_FORMAT = "\\draw[%s] (%s) -| node {%s} (%s);\n";
    protected static final String LABELLED_EDGE_FORMAT = "\\draw[%s] (%s) -| node[midway,sloped,above] {%s} (%s);\n";
    private static final String BOX_FORMAT = "\\draw[rounded corners, style=densely dotted] (%.1f,%.1f) rectangle (%.1f,%.1f);\n"
            + "\\node at (%.1f,%.1f) {\\textit{%s}};\n";

    private final Appendable output;
    private final String caption;
    private final String label;
    private final String placement;
    private final double scale;
    private final double nodeScale;
    private final int minNodeSize;

    private final StringBuilder nodeBuffer = new StringBuilder();
    private final StringBuilder cellBuffer = new StringBuilder();
    private final StringBuilder edgeBuffer = new StringBuilder();

    private final Set<NodeStyle> nodeStyles = new HashSet<NodeStyle>();
    private final Set<NodeStyle> cellStyles = new HashSet<NodeStyle>();

    private int nodeCounter = 0;

    protected int maxY = 0;
    protected int maxX = 0;
    private Double tableY = null;

    public AbstractTikzDrawing(Appendable output) {
        this(output, null, null, null, 0.6, 0.85, 19);
    }

    public AbstractTikzDrawing(Appendable output, String caption, String label, double scale, double nodeScale,
            int minNodeSize) {
        this(output, caption, label, "ht!", scale, nodeScale, minNodeSize);
    }

    public AbstractTikzDrawing(Appendable output, String caption, String label, String placement, double scale,
            double nodeScale, int minNodeSize) {
        this.output = output;
        this.caption = caption;
        this.label = label;
        this.placement = placement;
        this.scale = scale;
        this.nodeScale = nodeScale;
        this.minNodeSize = minNodeSize;
    }

    protected int incNodeCounter() {
        return nodeCounter++;
    }

    @Override
    public int drawNode(int x, int y, String label, NodeStyle style, boolean isTerminal) {
        if (y < maxY)
            maxY = y;
        if (x > maxX)
            maxX = x;
        return appendNode(x, y, label, style);
    }

    @Override
    public int drawCell(int x, int y, String label, NodeStyle style) {
        if (style.isBold())
            label = String.format(LABEL_FORMAT, label);
        return appendCell(x, y, label, style);
    }

    @Override
    public int drawCell(int x, int y, String label, String index, NodeStyle style) {
        return appendCell(x, y, String.format(INDEXED_LABEL_FORMAT, label, index), style);
    }

    @Override
    public void drawBox(String label) {
        drawBox(label, label.length());
    }

    @Override
    public void drawBox(String label, int labelLength) {
        if (tableY == null)
            tableY = maxY * NODE_Y_SCALE - 1.2;
        double y = tableY;
        cellBuffer.append(String.format(BOX_FORMAT, -0.6, y + 0.6, maxX + 0.6 + (double) labelLength / 3, y - 0.6, maxX
                + (double) labelLength / 4, y, label));
        tableY = tableY - 1.3;
    }

    protected int appendCell(int x, int y, String label, NodeStyle style) {
        if (tableY == null)
            tableY = maxY * NODE_Y_SCALE - 1.2;
        if (x > maxX)
            maxX = x;
        cellStyles.add(style);
        cellBuffer.append(String.format(CELL_FORMAT, nodeCounter, x, -y + tableY, style.getName(), label));

        return nodeCounter++;
    }

    public void appendEdge(int node, int parent, String format, String label, EdgeStyle style) {
        edgeBuffer.append(String.format(format, style, parent, label, node));
    }

    public void appendNode(int x, int y, int number, String label, NodeStyle style) {
        nodeStyles.add(style);
        nodeBuffer.append(String.format(NODE_FORMAT, number, x, y * NODE_Y_SCALE, style.getName(), label));
    }

    public int appendNode(int x, int y, String label, NodeStyle style) {
        nodeStyles.add(style);
        nodeBuffer.append(String.format(NODE_FORMAT, nodeCounter, x, y * NODE_Y_SCALE, style.getName(), label));

        return nodeCounter++;
    }

    @Override
    public void drawEdge(int node, int parent, String label, EdgeStyle style) {
        if (label.isEmpty())
            appendEdge(node, parent, getEdgeFormat(), label, style);
        else
            appendEdge(node, parent, getLabelledEdgeFormat(), label, style);
    }

    @Override
    public void drawEdge(int node, String nodeAnchor, int parent, String parentAnchor, EdgeStyle style) {
        edgeBuffer.append(String.format(EDGE_FORMAT, style, parent + "." + parentAnchor, "", node + "." + nodeAnchor));
    }

    @Override
    public void flush() throws IOException {
        if (caption != null && placement != null)
            output.append(String.format(FIGURE_HEADER, placement));
        output.append(TIKZ_HEADER);
        output.append(String.format(HEADER, scale, nodeScale, minNodeSize));
        output.append(formatStyles());
        output.append("]\n{\n");
        output.append(nodeBuffer);
        output.append(cellBuffer);
        output.append(edgeBuffer);
        output.append(FOOTER);
        output.append(TIKZ_FOOTER);
        if (caption != null && label != null)
            output.append(String.format(FIGURE_FOOTER, caption, label));
    }

    protected String getEdgeFormat() {
        return EDGE_FORMAT;
    }

    protected String getLabelledEdgeFormat() {
        return LABELLED_EDGE_FORMAT;
    }

    protected String formatStyles() {
        StringBuilder buffer = new StringBuilder();
        for (NodeStyle style : nodeStyles) {
            buffer.append(String.format("%s/.style={%s},\n", style.getName(), formatNodeStyle(style)));
        }
        for (NodeStyle style : cellStyles) {
            buffer.append(String.format("%s/.style={%s},\n", style.getName(), formatCellStyle(style)));
        }
        return buffer.toString();
    }

    protected String formatNodeStyle(NodeStyle style) {
        return style.toString();
    }

    protected String formatCellStyle(NodeStyle style) {
        return style.toString();
    }

}
